package com.pokermind.operator;

import com.pokermind.model.dto.*;
import com.pokermind.model.exception.HandManagementException;
import com.pokermind.model.po.PokerHand;
import com.pokermind.model.po.PokerHandAction;
import com.pokermind.model.po.PokerUser;
import com.pokermind.operator.validation.HandRequestValidator;
import com.pokermind.repository.dao.PokerHandDao;

import com.pokermind.repository.dao.PokerHandActionDao;
import com.pokermind.common.log.PlainLog;
import com.pokermind.common.log.TimeCostLog;
import com.pokermind.common.context.RequestContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 高级手牌管理业务操作类 实现完整的手牌生命周期管理
 *
 * @author PokerMind Team
 */
@Service
@Transactional
public class AdvancedHandOperator {

    private static final Logger logger = LoggerFactory.getLogger(AdvancedHandOperator.class);

    @Autowired
    private PokerHandDao pokerHandDao;

    @Autowired
    private PokerHandActionDao pokerHandActionDao;

    @Autowired
    private PokerUserOperator pokerUserOperator;
    
    /**
     * 手牌级别的锁，防止同一手牌的并发行动冲突
     * Key: handId, Value: Lock
     */
    private final Map<Long, Lock> handLocks = new ConcurrentHashMap<>();

    /**
     * 添加RequestId和ConnectionId到PlainLog中
     */
    /**
     * V4.7: 优化日志格式 - rqid放在最前面，整合请求内容
     * V4.7.1: 添加 tableHandId 唯一标识 (table_id/hand_id)
     */
    private PlainLog addRequestInfoToLog(PlainLog log) {
        String reqId = RequestContext.getRequestId();
        String connId = RequestContext.getConnectionId();
        String reqContent = RequestContext.getRequestContent();
        
        // V4.7.2: 优化日志字段顺序
        // 顺序：op → rqid → tableId → handId → tableHandId → 其他字段 → reqContent → connId
        PlainLog newLog = new PlainLog();
        
        // 1. op 操作类型（最重要，放在最前面）
        Object op = log.get("op");
        if (op != null) {
            newLog.put("op", op);
        }
        
        // 2. rqid 请求ID
        if (reqId != null) {
            newLog.put("rqid", reqId);
        }
        
        // 3. tableId 桌子ID
        Object tableId = log.get("tableId");
        if (tableId != null) {
            newLog.put("tableId", tableId);
        }
        
        // 4. handId 手牌ID
        Object handId = log.get("handId");
        if (handId != null) {
            newLog.put("handId", handId);
        }
        
        // 5. tableHandId 组合唯一标识
        if (tableId != null && handId != null) {
            newLog.put("tableHandId", tableId + "/" + handId);
        }
        
        // 6. 复制其他字段（排除已处理的字段）
        log.forEach((key, value) -> {
            if (!"op".equals(key) && !"tableId".equals(key) && !"handId".equals(key)) {
                newLog.put(key, value);
            }
        });
        
        // 7. reqContent（不格式化，单行）
        if (reqContent != null) {
            newLog.put("reqContent", reqContent);
        }
        
        // 8. connId 连接ID（放在最后）
        if (connId != null) {
            newLog.put("connId", connId);
        }
        
        return newLog;
    }

    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 创建新手牌
     */
    @Transactional
    public CreateHandResponse createHand(CreateHandRequest request) {
        PlainLog log = new TimeCostLog("op", "create_hand")
            .put("tableId", request.getTableId())
            .put("handId", request.getHandId())
            .put("players", request.getPlayers().size());
        // ⚡ 简化：去掉昂贵的JSON序列化，直接记录关键字段
        // request 信息已在后续的日志中体现
        
        try {
            // 统一验证请求数据
            // V4.7: 验证 (table_id, hand_id) 唯一性
            HandRequestValidator.validateCreateHandRequest(request, this::handExists);
            // 确保所有用户记录
            ensureAllPlayersExist(request.getPlayers());
            
            // Hero概念已移除，改用第一个玩家的信息（临时方案）
            // TODO: 重构为不依赖单一hero的架构
            CreateHandRequest.PlayerInfo firstPlayer = request.getPlayers().isEmpty() ? null : request.getPlayers().get(0);
            if (firstPlayer == null) {
                throw new HandManagementException.NoHeroPlayerException(); // 复用异常，实际含义为"无玩家"
            }
            
            // 创建手牌实体 - 使用第一个玩家作为默认用户ID来源
            Long userId = findUserIdByPlayerId(firstPlayer.getPlayerId());
            if (userId == null) {
                logger.warn("无法找到玩家的用户ID: playerId={}, 使用默认值", firstPlayer.getPlayerId());
                userId = 1L; // 默认用户ID
            }
            
            // V4.8: 创建手牌时不再传入 smallBlind 和 bigBlind（构造函数简化）
            PokerHand hand = new PokerHand(request.getHandId(), request.getTableId(), userId, request.getButtonSeat(),
                request.getPlayers().size());
            
            // 设置初始状态（hero相关字段可选）
            hand.setHeroSeatNumber(firstPlayer.getSeatNumber()); // 仍使用hero字段，但含义已变
            hand.setHeroStackStart(firstPlayer.getStack());
            
            // V4.8: 记录盲注配置（用于后续分析），但不自动创建盲注行动
            hand.setSmallBlind(request.getSmallBlind());
            hand.setBigBlind(request.getBigBlind());
            hand.setAnte(request.getAnte());
            
            String playerPosition = firstPlayer.getPosition();
            if (playerPosition != null) {
                hand.setHeroPosition(playerPosition); // 字段名未改，但不再是hero概念
            }
            hand.setCurrentPot(calculateInitialPot(request));
            // 注意：下一个行动座位、活跃玩家数、玩家信息等字段已从表中移除
            // V4.1: 不再在创建手牌时设置hole cards（改为AI分析时提供）
            // 序列化完整玩家信息 - 用于手牌重建和分析
            try {
                String playersInfoJson = objectMapper.writeValueAsString(request.getPlayers());
                hand.setPlayersInfo(playersInfoJson);
            } catch (JsonProcessingException e) {
                logger.error("JSON序列化失败: {}", e.getMessage());
                throw new HandManagementException.JsonParseException();
            }
            // Save to database
            PokerHand savedHand = savePokerHand(hand);
            
            // V4.8: 不再自动记录盲注行动
            // 前端需通过 RECORD_ACTION_REQ 主动发起小盲、大盲、Straddle 行动
            // createBlindActions(savedHand, request);  // ❌ Removed in V4.8
            
            // Build response - V4.10: 只返回核心字段
            CreateHandResponse response = new CreateHandResponse(request.getTableId(), request.getHandId());
            
            logger.info(addRequestInfoToLog(log.put("handRecordId", savedHand.getId()).put("playerPosition", playerPosition)
                .put("pot", savedHand.getCurrentPot())).put("op_rslt", "success").toString());
            return response;
        } catch (Exception e) {
            log.put("op_rslt", "failed").put("error", e.getMessage());
            logger.error(addRequestInfoToLog(log).toString(), e);
            throw e;
        }
    }

    /**
     * 更新Hero底牌
     * V4.7: 添加 tableId 参数以支持 (table_id, hand_id) 唯一性
     */
    @Transactional
    public void updateHeroCards(String handId, String tableId, List<String> holeCards) {
        PokerHand hand = findHandByHandId(handId, tableId);
        validateHoleCards(holeCards);

        try {
            hand.setHeroHoleCards(objectMapper.writeValueAsString(holeCards));
            updatePokerHand(hand);
        } catch (Exception e) {
            throw new RuntimeException("Failed to update hero cards", e);
        }
    }

    /**
     * 街道转换 - 德州扑克核心逻辑 基于"前端主导，后端辅助"设计原则： - 前端负责：游戏逻辑执行、底池计算、玩家状态跟踪 - 后端负责：数据持久化、状态记录、基础验证、审计追踪
     */
    @Transactional
    public StreetTransitionResponse transitionToStreet(String handId, StreetTransitionRequest request) {
        PokerHand hand = findHandByHandId(handId, request.getTableId());
        PlainLog log = new TimeCostLog("api", "street_transition")
            .put("tableId", hand.getTableId())
            .put("handId", hand.getHandId())
            .put("targetStreet", request.getStreet())
            .put("activePlayers", request.getActivePlayers())
            .put("boardCardsCount", request.getBoardCards() != null ? request.getBoardCards().size() : 0)
            .put("potAfter", request.getPotAfter());
        // ⚡ 简化：去掉昂贵的JSON序列化，请求信息已在 api:street_transition 日志中体现
        
        try {
            // 1. 获取手牌记录并记录当前状态 - 已在上面获取
            BigDecimal previousPot = hand.getCurrentPot();
            String currentStreet = hand.getCurrentStreet();
            // 注意：活跃玩家数字段已移除，使用请求中的数据
            Integer previousActivePlayers = request.getActivePlayers(); // 使用前端提供的数据
            log.put("currentStreet", currentStreet).put("previousPot", previousPot)
                .put("previousActivePlayers", previousActivePlayers);

            // 2. 基础空值检查（只检查数据完整性，信任前端逻辑）
            validateDataIntegrity(hand, request);
            log.put("dataIntegrityPassed", true);

            // 3. 更新底池信息（前端计算，后端存储）
            if (request.getPotAfter() != null) {
                hand.setCurrentPot(request.getPotAfter());
                log.put("potUpdated", true).put("potChange",
                    request.getPotAfter().subtract(previousPot != null ? previousPot : BigDecimal.ZERO));
            }

            // 4. 更新手牌状态到新街道
            String serializedBoardCards = serializeBoardCards(request.getBoardCards());
            // 更新手牌状态到新街道（优化版）
            hand.advanceToStreet(request.getStreet(), serializedBoardCards, request.getActivePlayers());

            // 5. 记录前端提供的状态信息（不进行后端计算）
            log.put("frontendProvidedStreet", request.getStreet());

            // 6. 记录公共牌和玩家变化
            log.put("boardCards", request.getBoardCards().toString()).put("activePlayersChange",
                request.getActivePlayers() - (previousActivePlayers != null ? previousActivePlayers : 0));

            // 7. 保存更新后的状态
            updatePokerHand(hand);

            // 8. 构建简化响应 - V4.10: 只返回核心字段
            StreetTransitionResponse response = new StreetTransitionResponse(request.getTableId(), request.getHandId());
            
            // 9. 记录成功日志
            log.put("finalPot", hand.getCurrentPot()).put("finalActivePlayers", request.getActivePlayers())
                .put("transitionSuccess", true).put("op_rslt", "success");
            logger.info(addRequestInfoToLog(log).toString());
            return response;
        } catch (IllegalArgumentException e) {
            log.put("op_rslt", "validation_error").put("error", e.getMessage());
            logger.warn(addRequestInfoToLog(log).toString());
            throw e;
        } catch (HandManagementException e) {
            log.put("op_rslt", "business_error").put("errorCode", e.getErrorCode()).put("error", e.getMessage());
            logger.warn(addRequestInfoToLog(log).toString());
            throw e;
        } catch (Exception e) {
            log.put("op_rslt", "system_error").put("error", e.getMessage());
            logger.error(addRequestInfoToLog(log).toString(), e);
            throw new RuntimeException("Street transition failed", e);
        }
    }

    /**
     * 记录单个行动 - 德州扑克Hero视角核心方法 完整的行动记录流程： 1. 数据验证 - 确保行动请求的合法性 2. 行动记录 - 保存行动到数据库 3. 状态更新 - 更新手牌的各种状态 4. Hero弃牌检测 -
     * 检测是否为Hero弃牌，如果是则自动完成手牌 5. 响应构建 - 根据情况构建普通响应或手牌完成响应 Hero视角特性： - 当检测到Hero弃牌时，自动触发completeHand流程 -
     * 返回特殊的"手牌已完成"响应，通知前端手牌结束 - 不再需要等待其他玩家的后续行动
     */
    public ActionResponse recordAction(String handId, ActionRequest request) {
        PokerHand hand = findHandByHandId(handId, request.getTableId());
        
        // 🔒 获取手牌级别的锁，防止并发冲突
        Lock lock = handLocks.computeIfAbsent(hand.getId(), k -> new ReentrantLock());
        lock.lock();
        
        try {
            // ⚡ 性能优化：缓存常用字段到局部变量，避免重复getter调用
            String playerId = request.getPlayerId();
            String actionType = request.getActionType();
            
            // 统一日志：所有action都使用record_action
            PlainLog log = new TimeCostLog("op", "record_action")
                .put("tableId", hand.getTableId())
                .put("handId", handId)
                .put("street", request.getStreet())  // 前端驱动：直接使用前端上传的街道信息
                .put("playerId", playerId)
                .put("actionType", actionType);
            
            // 牌局状态信息（所有action都记录）
            if (hand.getActivePlayers() != null) {
                log.put("activePlayers", hand.getActivePlayers());
            }
            
            // 必要的请求信息（不序列化JSON，直接添加字段）
            if (request.getAmount() != null && request.getAmount().compareTo(BigDecimal.ZERO) > 0) {
                log.put("amount", request.getAmount());
            }
            if (request.getAfterStack() != null) {
                log.put("stackAfter", request.getAfterStack());
            }
            if (request.getAfterPot() != null) {
                log.put("potAfter", request.getAfterPot());
            }

            try {
            // 1. 获取手牌记录 - 已在上面获取

            // 2. 基础数据完整性检查 - 只验证必要字段，信任前端业务逻辑
            validateActionDataIntegrity(request);

            // 3. 创建并保存行动记录
            PokerHandAction action = createActionRecord(hand, request);
            PokerHandAction savedAction = pokerHandActionDao.save(action);
            log.put("savedActionId", savedAction.getId());

            // 4. 更新手牌状态 - 统一处理状态同步
            updateHandStateAndPersistSimplified(hand, request);

            // 5. V4.7：移除服务端自动检测逻辑，改为客户端主动调用 complete_hand
            // 服务端只负责记录 action，不主动完成手牌

            // 6. 普通情况：构建简化的行动响应 - V4.10: 只返回核心字段
            ActionResponse response = new ActionResponse(request.getTableId(), request.getHandId());
            
            // 记录成功完成，添加牌局状态信息
            log.put("handStatus", "active")  // 牌局状态：进行中
                .put("handClosed", false)     // 牌局未结束
                .put("op_rslt", "success");
                logger.info(addRequestInfoToLog(log).toString());
                return response;
            } catch (IllegalArgumentException e) {
                log.put("op_rslt", "validation_error").put("error", e.getMessage());
                logger.warn(addRequestInfoToLog(log).toString());
                throw e;
            } catch (Exception e) {
                log.put("op_rslt", "system_error").put("error", e.getMessage());
                logger.error(addRequestInfoToLog(log).toString(), e);
                throw new RuntimeException("记录行动失败", e);
            }
        } finally {
            // 🔓 释放锁
            lock.unlock();
        }
    }

    // ########################私有辅助方法

    /**
     * 完成手牌 - V4.7优化：自动分配底池并更新用户统计
     */
    public HandCompletionResponse completeHand(String handId, HandCompletionRequest request) {
        // V4.7：直接调用底池分配方法
        return completeHandWithPotDistribution(handId, request);
    }

    /**
     * V4.7新增：检查游戏是否结束
     * 
     * 检查逻辑：
     * 1. 统计所有玩家在当前街道及之前的 fold 动作
     * 2. 如果只剩一个玩家没有 fold，游戏结束
     * 
     * @param handRecordId 手牌记录ID
     * @param currentStreet 当前街道
     * @return 游戏是否结束
     */
    private boolean checkIfHandFinished(Long handRecordId, String currentStreet) {
        try {
            // 查询所有 action
            List<PokerHandAction> allActions = pokerHandActionDao.findByHandId(handRecordId);
            
            // 统计弃牌的玩家
            Set<String> foldedPlayers = new HashSet<>();
            Set<String> allPlayers = new HashSet<>();
            
            for (PokerHandAction action : allActions) {
                String playerId = action.getPlayerId();
                allPlayers.add(playerId);
                
                if (PokerHandAction.ACTION_FOLD.equalsIgnoreCase(action.getActionType())) {
                    foldedPlayers.add(playerId);
                }
            }
            
            // 计算剩余玩家数
            int remainingPlayers = allPlayers.size() - foldedPlayers.size();
            
            logger.debug("游戏状态检查 - handRecordId: {}, 总玩家: {}, 弃牌玩家: {}, 剩余玩家: {}", 
                handRecordId, allPlayers.size(), foldedPlayers.size(), remainingPlayers);
            
            // 只剩一个玩家，游戏结束
            return remainingPlayers == 1;
        } catch (Exception e) {
            logger.error("检查游戏是否结束时出错，handRecordId: {}", handRecordId, e);
            return false;
        }
    }
    
    /**
     * V4.7新增：构建自动完成请求
     * 
     * @param hand 手牌对象
     * @param lastAction 最后一个动作
     * @return 完成请求
     */
    private HandCompletionRequest buildAutoCompletionRequest(PokerHand hand, ActionRequest lastAction) {
        HandCompletionRequest request = new HandCompletionRequest();
        
        try {
            request.setTableId(hand.getTableId());
            request.setHandId(hand.getHandId());
            request.setCompletionType("auto_fold");
            
            // 设置最终底池 - 使用 after_pot 或 current_pot
            BigDecimal finalPot = (lastAction.getAfterPot() != null) 
                ? lastAction.getAfterPot() 
                : hand.getCurrentPot();
            request.setFinalPot(finalPot != null ? finalPot : BigDecimal.ZERO);
            
            // 暂不收取抽水
            request.setRakeAmount(BigDecimal.ZERO);
            
            logger.info("构建自动完成请求成功 - 手牌ID: {}, 最终底池: {}", hand.getHandId(), finalPot);
            
            return request;
        } catch (Exception e) {
            logger.error("构建自动完成请求失败，手牌ID: {}", hand.getHandId(), e);
            // 返回基础请求
            HandCompletionRequest fallbackRequest = new HandCompletionRequest();
            fallbackRequest.setTableId(hand.getTableId());
            fallbackRequest.setHandId(hand.getHandId());
            fallbackRequest.setCompletionType("auto_fold");
            fallbackRequest.setFinalPot(hand.getCurrentPot() != null ? hand.getCurrentPot() : BigDecimal.ZERO);
            fallbackRequest.setRakeAmount(BigDecimal.ZERO);
            return fallbackRequest;
        }
    }
    
    /**
     * V4.7新增：完成手牌并分配底池给赢家
     * 
     * 核心逻辑：
     * 1. 找到最后剩余的玩家（没有 fold 的玩家）
     * 2. 将底池分配给该玩家
     * 3. 更新玩家的 stack_after
     * 4. 更新用户统计数据
     * 
     * @param handId 手牌ID
     * @param request 完成请求
     * @return 完成响应
     */
    @Transactional
    public HandCompletionResponse completeHandWithPotDistribution(String handId, HandCompletionRequest request) {
        PokerHand hand = findHandByHandId(handId, request.getTableId());
        
        // V4.7.2: 使用 PlainLog 统一日志格式，op字段标识操作类型
        PlainLog log = new TimeCostLog("op", "complete_hand")
            .put("tableId", hand.getTableId())
            .put("handId", handId)
            .put("completionType", request.getCompletionType())
            .put("playersToShowdown", request.getPlayersToShowdown());
        
        try {
            // ✅ V4.7简化：完全信任前端数据，不进行任何复杂计算
            // 前端已经在final_results中提供了完整的最终状态
            
            // 1. 直接使用前端提供的 final_pot（不需要计算）
            BigDecimal finalPot = request.getFinalPot();
            log.put("finalPot", finalPot);
            
            // 2. 更新手牌状态（使用前端提供的最终底池）
            hand.complete(null, null, finalPot, null, null);
            hand.setRakeAmount(request.getRakeAmount());
            
            // 3. ✅ V4.8新增：直接使用前端的 final_results 构建 JSON 并保存
            if (request.getFinalResults() != null && !request.getFinalResults().isEmpty()) {
                Map<String, Object> finalResultsMap = new LinkedHashMap<>();
                List<Map<String, Object>> results = new ArrayList<>();
                
                for (HandCompletionRequest.FinalResult result : request.getFinalResults()) {
                    Map<String, Object> item = new LinkedHashMap<>();
                    item.put("player_id", result.getPlayerId());
                    item.put("seat_number", result.getSeatNumber());
                    item.put("player_name", result.getPlayerName());
                    item.put("cards", convertCardsListToString(result.getCards()));
                    item.put("cards_shown", convertCardsListToString(result.getCardsShown()));
                    item.put("net_result", result.getNetResult());
                    item.put("final_stack", result.getFinalStack());
                    item.put("hand_rank", result.getHandRank());
                    item.put("hand_description", result.getHandDescription());
                    results.add(item);
                }
                
                finalResultsMap.put("results", results);
                
                // 转换为 JSON 字符串并保存
                String jsonString = objectMapper.writeValueAsString(finalResultsMap);
                hand.setFinalResults(jsonString);
                log.put("finalResultsCount", results.size());
            }
            
            // 4. ✅ V4.5新增：处理 Run It Twice
            if (request.getRunItTwice() != null && 
                request.getRunItTwice().getEnabled() != null &&
                request.getRunItTwice().getEnabled()) {
                
                hand.setRunItTwice(true);
                
                // 保存 Run It Twice 详细结果
                String runTwiceJson = objectMapper.writeValueAsString(request.getRunItTwice());
                hand.setRunTwiceDetails(runTwiceJson);
                
                int runsCount = request.getRunItTwice().getRuns() != null ? 
                    request.getRunItTwice().getRuns().size() : 0;
                log.put("runItTwice", true).put("runsCount", runsCount);
            }
            
            updatePokerHand(hand);
            
            // 4. 从 final_results 中推断赢家（net_result > 0 为赢家）
            String winner = null;
            if (request.getFinalResults() != null) {
                for (HandCompletionRequest.FinalResult result : request.getFinalResults()) {
                    if (result.getNetResult() != null && 
                        result.getNetResult().compareTo(BigDecimal.ZERO) > 0) {
                        winner = result.getPlayerId();
                        break;
                    }
                }
            }
            
            // 5. 更新所有玩家的统计数据（基于前端提供的数据）
            List<PokerHandAction> allActions = pokerHandActionDao.findByHandId(hand.getId());
            updateAllPlayersStatistics(hand, allActions, winner, finalPot);
            
            // 6. 构建响应
            int totalActions = allActions.size();
            int durationSeconds = calculateHandDuration(hand);
            HandStatistics statistics = new HandStatistics(totalActions, calculateStreetsReached(hand), null);
            
            // 添加成功日志的字段
            log.put("winner", winner)
                .put("totalActions", totalActions)
                .put("durationSeconds", durationSeconds)
                .put("op_rslt", "success");
            
            logger.info(addRequestInfoToLog(log).toString());
            
            // V4.10: 只返回核心字段
            HandCompletionResponse response = new HandCompletionResponse(request.getTableId(), request.getHandId());
            
            return response;
        } catch (Exception e) {
            log.put("op_rslt", "failed").put("error", e.getMessage());
            logger.error(addRequestInfoToLog(log).toString(), e);
            throw new RuntimeException("完成手牌失败", e);
        }
    }
    
    /**
     * V4.7新增：更新所有玩家的统计数据
     * 
     * @param hand 手牌对象
     * @param allActions 所有动作
     * @param winner 赢家
     * @param pot 底池
     */
    private void updateAllPlayersStatistics(PokerHand hand, List<PokerHandAction> allActions, 
                                            String winner, BigDecimal pot) {
        try {
            // 统计每个玩家的投入
            Map<String, BigDecimal> playerInvestments = new HashMap<>();
            Map<String, BigDecimal> playerStartStacks = new HashMap<>();
            
            // 从 players_info 获取初始筹码
            String playersInfoJson = hand.getPlayersInfo();
            if (playersInfoJson != null && !playersInfoJson.isEmpty()) {
                try {
                    List<Map<String, Object>> playersInfo = objectMapper.readValue(
                        playersInfoJson, 
                        objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
                    );
                    
                    for (Map<String, Object> playerInfo : playersInfo) {
                        String playerId = (String) playerInfo.get("player_id");
                        Object stackObj = playerInfo.get("stack");
                        BigDecimal startStack = stackObj instanceof Number 
                            ? BigDecimal.valueOf(((Number) stackObj).doubleValue())
                            : BigDecimal.ZERO;
                        
                        playerStartStacks.put(playerId, startStack);
                        playerInvestments.put(playerId, BigDecimal.ZERO);
                    }
                } catch (Exception e) {
                    logger.error("解析 players_info 失败", e);
                }
            }
            
            // 计算每个玩家的投入（initial stack - current stack）
            for (PokerHandAction action : allActions) {
                String playerId = action.getPlayerId();
                
                // 跳过没有投入的动作
                if (action.getAmount() == null || action.getAmount().compareTo(BigDecimal.ZERO) == 0) {
                    continue;
                }
                
                BigDecimal currentInvestment = playerInvestments.getOrDefault(playerId, BigDecimal.ZERO);
                playerInvestments.put(playerId, currentInvestment.add(action.getAmount()));
            }
            
            // 更新每个玩家的统计
            for (String playerId : playerInvestments.keySet()) {
                Long userId = findUserIdByPlayerId(playerId);
                if (userId == null) {
                    logger.warn("无法找到用户ID，playerId: {}", playerId);
                    continue;
                }
                
                Optional<PokerUser> userOpt = pokerUserOperator.findUserById(userId);
                if (!userOpt.isPresent()) {
                    logger.warn("无法找到用户，userId: {}", userId);
                    continue;
                }
                
                PokerUser user = userOpt.get();
                
                // 更新 total_hands
                int currentTotalHands = user.getTotalHands() != null ? user.getTotalHands() : 0;
                user.setTotalHands(currentTotalHands + 1);
                
                // 计算盈亏
                BigDecimal investment = playerInvestments.get(playerId);
                BigDecimal profit;
                
                if (playerId.equals(winner)) {
                    // 赢家：底池 - 投入
                    profit = pot.subtract(investment);
                } else {
                    // 输家：-投入
                    profit = investment.negate();
                }
                
                // 更新 total_profit
                BigDecimal currentProfit = user.getTotalProfit() != null ? user.getTotalProfit() : BigDecimal.ZERO;
                user.setTotalProfit(currentProfit.add(profit));
                
                // 保存更新
                pokerUserOperator.updateUser(user);
            }
        } catch (Exception e) {
            // V4.7.2: 移除玩家统计日志，减少日志噪音
            // 不抛出异常，让手牌完成继续进行
        }
    }
    
    /**
     * @deprecated V4.10 已废弃 - ActionResponse已简化，不再需要此方法
     */
    /*
    private ActionResponse buildFallbackActionResponse(PokerHandAction savedAction, PokerHand hand,
        boolean isHeroFold) {
        try {
            ActionResponse.SequenceInfo sequence =
                new ActionResponse.SequenceInfo(savedAction.getActionSequence(), savedAction.getGlobalSequence(),
                    savedAction.getGlobalSequence() + "/HERO_FOLD" // 特殊标识
                );

            ActionResponse.GameState gameState = buildGameState(hand);
            if (isHeroFold) {
                // TODO: 添加Hero弃牌相关的状态标识（需要扩展GameState类）
                // gameState.setRequiresCompletion(true); // 标识需要前端处理完成
                // gameState.setCompletionType("hero_fold");
                logger.info("Hero弃牌降级处理 - 需要前端识别HERO_FOLD标识");
            }

            ActionResponse.ValidationResult validationResult =
                new ActionResponse.ValidationResult(true, true, !isHeroFold);

            return new ActionResponse(savedAction.getId(), sequence, gameState, validationResult);
        } catch (Exception e) {
            logger.error("构建降级响应失败，行动ID: {}", savedAction.getId(), e);
            // 最基础的响应
            return new ActionResponse(savedAction.getId(),
                new ActionResponse.SequenceInfo(1, savedAction.getGlobalSequence(), "ERROR"),
                new ActionResponse.GameState(), new ActionResponse.ValidationResult(true, false, false));
        }
    }
    */

    // @Deprecated - Hero概念已移除
    // private CreateHandRequest.PlayerInfo findHeroPlayer(List<CreateHandRequest.PlayerInfo> players) {
    //     return players.stream().filter(p -> Boolean.TRUE.equals(p.getIsHero())).findFirst().orElse(null);
    // }

    /**
     * @deprecated 错误的设计，会导致数据一致性问题。应使用 findUserIdByPlayerId 查询数据库获取真实ID 从Hero玩家ID中提取用户ID 支持格式：纯数字 "123" 或带前缀
     *     "user_123", "hero_456"
     */
    @Deprecated
    private Long extractUserIdFromHero(String playerId) {
        if (playerId == null || playerId.trim().isEmpty()) {
            throw new IllegalArgumentException("Hero player ID cannot be null or empty");
        }

        String cleanPlayerId = playerId.trim();

        // 尝试直接解析为数字
        try {
            return Long.parseLong(cleanPlayerId);
        } catch (NumberFormatException e) {
            // 处理带前缀的格式：user_123, hero_456, player_789
            if (cleanPlayerId.contains("_")) {
                String[] parts = cleanPlayerId.split("_");
                String lastPart = parts[parts.length - 1];
                try {
                    return Long.parseLong(lastPart);
                } catch (NumberFormatException ex) {
                    logger.warn("Unable to parse numeric user ID from playerId: {}, using hash fallback", playerId);
                    return Math.abs((long)playerId.hashCode());
                }
            }
            // 如果没有下划线，使用哈希作为fallback
            logger.warn("Invalid playerId format: {}, using hash fallback", playerId);
            return Math.abs((long)playerId.hashCode());
        }
    }

    /**
     * V4.8: 计算初始底池
     * 盲注改为前端主动发起，初始底池固定为 0
     * 底池金额将通过 RECORD_ACTION_REQ 逐步累积
     */
    private BigDecimal calculateInitialPot(CreateHandRequest request) {
        // V4.8: 盲注由前端主动发起，初始底池为 0
        return BigDecimal.ZERO;
        
        // V4.7 及之前的逻辑（已废弃）：
        // BigDecimal pot = request.getSmallBlind().add(request.getBigBlind());
        // if (request.getAnte() != null) {
        //     pot = pot.add(request.getAnte().multiply(BigDecimal.valueOf(request.getPlayers().size())));
        // }
        // if (request.getStraddle() != null && request.getStraddle().getMultiplier() != null 
        //     && request.getStraddle().getMultiplier() > 0) {
        //     BigDecimal straddleAmount = request.getBigBlind()
        //         .multiply(BigDecimal.valueOf(request.getStraddle().getMultiplier()));
        //     pot = pot.add(straddleAmount);
        // }
        // return pot;
    }

    /**
     * V4.7: 检查 (table_id, hand_id) 是否存在
     * 支持同一个 hand_id 在不同 table_id 中存在
     */
    private boolean handExists(String handId, String tableId) {
        return pokerHandDao.existsByHandIdAndTableId(handId, tableId);
    }

    /**
     * V4.7: 根据 (table_id, hand_id) 查找手牌
     * 支持同一个 hand_id 在不同 table_id 中存在
     */
    private PokerHand findHandByHandId(String handId, String tableId) {
        return pokerHandDao.findByHandIdAndTableId(handId, tableId)
            .orElseThrow(() -> new IllegalArgumentException("Hand not found: " + handId + " at table: " + tableId));
    }
    private PokerHand findHandById(Long handRecordId) {
        return pokerHandDao.findById(handRecordId)
            .orElseThrow(() -> new IllegalArgumentException("Hand not found with ID: " + handRecordId));
    }

    private PokerHand savePokerHand(PokerHand hand) {
        return pokerHandDao.save(hand);
    }

    private void updatePokerHand(PokerHand hand) {
        int updated = pokerHandDao.update(hand);
        if (updated == 0) {
            throw new IllegalArgumentException("Hand not found for update with ID: " + hand.getId());
        }
    }

    private void validateHoleCards(List<String> holeCards) {
        if (holeCards == null || holeCards.size() != 2) {
            throw new IllegalArgumentException("Must provide exactly 2 hole cards");
        }

        // 验证牌面格式
        for (String card : holeCards) {
            if (!isValidCard(card)) {
                throw new IllegalArgumentException("Invalid card format: " + card);
            }
        }
    }

    private boolean isValidCard(String card) {
        if (card == null || card.length() != 2) {
            return false;
        }

        char rank = card.charAt(0);
        char suit = card.charAt(1);

        String validRanks = "23456789TJQKA";
        String validSuits = "shdc";

        return validRanks.indexOf(rank) >= 0 && validSuits.indexOf(suit) >= 0;
    }

    // 移除validateStreetTransition方法 - 违反"前端主导"原则
    // 街道转换验证完全由前端负责

    /**
     * 数据完整性检查 - 纯前端主导模式，只检查空值 完全信任前端的业务逻辑，后端只负责数据存储
     */
    private void validateDataIntegrity(PokerHand hand, StreetTransitionRequest request) {
        // 只进行最基本的空值检查，不涉及任何业务逻辑
        if (request.getStreet() == null || request.getStreet().trim().isEmpty()) {
            throw new IllegalArgumentException("Street cannot be null or empty");
        }

        if (request.getBoardCards() == null) {
            throw new IllegalArgumentException("Board cards cannot be null");
        }

        if (request.getActivePlayers() == null) {
            throw new IllegalArgumentException("Active players cannot be null");
        }
    }

    // 移除isValidStreetTransition方法 - 违反"前端主导"原则
    // 街道转换验证应该由前端负责

    // 移除validateBoardCardsForStreet方法 - 违反"前端主导"原则
    // 公共牌数量和格式验证应该由前端负责

    // 移除validateActivePlayersTransition方法 - 违反"前端主导"原则
    // 活跃玩家数量变化验证应该由前端负责

    private String serializeBoardCards(List<String> boardCards) {
        try {
            return objectMapper.writeValueAsString(boardCards);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize board cards", e);
        }
    }

    private String serializeFinalResults(List<HandCompletionRequest.FinalResult> finalResults) {
        try {
            return objectMapper.writeValueAsString(finalResults);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize final results", e);
        }
    }

    private BoardTexture analyzeBoardTexture(List<String> boardCards) {
        // TODO: 实现牌面结构分析
        return new BoardTexture();
    }

    // 移除calculatePostFlopFirstAction方法 - 违反"前端主导"原则
    // 后端不应该计算游戏逻辑，完全交给前端处理

    /**
     * 行动数据完整性检查 - 纯前端主导模式 只检查基础的数据完整性，完全信任前端的业务逻辑
     *
     * @param request 行动请求
     * @throws IllegalArgumentException 数据完整性失败时抛出
     */
    private void validateActionDataIntegrity(ActionRequest request) {
        // 只进行最基础的空值检查，完全信任前端业务逻辑
        // 注意: seat_number已被移除，不再进行座位号验证

        if (request.getActionType() == null || request.getActionType().trim().isEmpty()) {
            throw new IllegalArgumentException("行动类型不能为空");
        }

        if (request.getPlayerId() == null || request.getPlayerId().trim().isEmpty()) {
            throw new IllegalArgumentException("玩家ID不能为空");
        }
    }

    // 移除isValidActionType方法 - 违反"前端主导"原则
    // 行动类型验证应该由前端负责

    // 移除validateActionAmount方法 - 违反"前端主导"原则
    // 行动金额验证应该由前端负责

    /**
     * 检测是否为Hero的弃牌行动 - 德州扑克Hero视角核心逻辑
     *
     * Hero视角系统的关键特性： - 当Hero弃牌时，手牌立即结束（从Hero角度来看） - 不再记录其他玩家的后续行动 - 自动触发手牌完成流程
     *
     * @param hand    当前手牌记录
     * @param request 行动请求
     * @return true表示这是Hero的弃牌行动
     */
    private boolean isHeroFoldAction(PokerHand hand, ActionRequest request) {
        try {
            // 1. 检查行动类型是否为弃牌
            if (!PokerHandAction.ACTION_FOLD.equalsIgnoreCase(request.getActionType())) {
                return false;
            }
            // 2. 检查执行行动的玩家是否为Hero
            // 主要方法：通过玩家ID对比（推荐）
            // 方法二：通过玩家ID对比（备用方法）
            // 从手牌的玩家信息中找到Hero的ID进行对比
            if (request.getPlayerId() != null) {
                boolean isHeroById = isHeroPlayerById(hand, request.getPlayerId());
                if (isHeroById) {
                    logger.info("检测到Hero弃牌行动：玩家ID匹配 - 手牌ID: {}, 弃牌玩家ID: {}", hand.getId(),
                        request.getPlayerId());
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            logger.warn("检测Hero弃牌行动时发生异常，手牌ID: {}", hand.getId(), e);
            return false; // 异常时不认为是Hero弃牌，保证系统稳定
        }
    }

    /**
     * 通过玩家名称检测是否为Hero玩家 解析手牌的玩家信息JSON，找到Hero玩家的名称进行对比
     *
     * @deprecated 建议使用 isHeroPlayerById 方法，基于playerId更准确
     */
    private boolean isHeroPlayerByName(PokerHand hand, String playerName) {
        try {
            // 注意：playersInfo字段已移除，无法从手牌记录中获取玩家信息
            // 建议传入 playerId 而非 playerName 进行匹配
            logger.warn("playersInfo field has been removed, cannot validate by playerName. Use playerId instead.");
            return false; // 无法验证，返回 false
        } catch (Exception e) {
            logger.warn("解析玩家信息时发生异常，手牌ID: {}", hand.getId(), e);
            return false;
        }
    }

    /**
     * 通过玩家ID检测是否为Hero玩家 - 推荐方法 解析手牌的玩家信息JSON，找到Hero玩家的ID进行对比
     *
     * @param hand     手牌记录
     * @param playerId 待检测的玩家ID
     * @return true表示该playerId对应Hero玩家
     */
    private boolean isHeroPlayerById(PokerHand hand, String playerId) {
        try {
            // 注意：playersInfo字段已移除，改用 userId 对比验证
            if (playerId == null) {
                return false;
            }
            // 使用用户ID对比验证（需要先查询 playerId 对应的 userId）
            try {
                Long playerUserId = findUserIdByPlayerId(playerId);
                return hand.getUserId().equals(playerUserId);
            } catch (Exception e) {
                logger.warn("Failed to validate Hero player by ID: {}", playerId, e);
                return false;
            }

            // 原有的players遍历代码已移除
        } catch (Exception e) {
            logger.warn("解析玩家信息时发生异常，手牌ID: {}", hand.getId(), e);
            return false;
        }
    }

    /**
     * 构建Hero弃牌时的自动完成请求 - 德州扑克Hero视角业务逻辑
     *
     * Hero弃牌场景的特殊处理： - completion_type: "hero_fold" - 标识这是Hero弃牌导致的手牌结束 - hero_final_action: "fold" - 明确记录Hero的最后行动 -
     * hero_went_to_showdown: false - Hero弃牌必定不会到摊牌 - players_to_showdown: 0 - 从Hero视角来看，不关心其他玩家的摊牌 - hero_net_result:
     * 负数 - Hero弃牌意味着损失已投入的筹码
     */
    private HandCompletionRequest buildPlayerFoldCompletionRequest(PokerHand hand, ActionRequest foldRequest) {
        HandCompletionRequest request = new HandCompletionRequest();

        try {
            // 1. 设置完成类型为玩家弃牌
            request.setCompletionType("player_fold");

            // 2-5. Hero概念已移除，不再记录hero相关字段
            // TODO: 如果需要记录玩家结果，使用final_results数组

            // 6. 设置最终底池 - 使用当前底池作为最终底池
            BigDecimal finalPot =
                (foldRequest.getAfterPot() != null) ? foldRequest.getAfterPot() : hand.getCurrentPot();
            request.setFinalPot(finalPot != null ? finalPot : BigDecimal.ZERO);

            // 7. 玩家弃牌通常没有抽水（抽水在有人赢得底池时收取）
            request.setRakeAmount(BigDecimal.ZERO);

            // 8. 不需要设置final_results
            // 注意：finalResults字段从 poker_hand_actions 表计算

            logger.info("构建玩家弃牌完成请求成功 - 手牌ID: {}, 最终底池: {}", hand.getId(), finalPot);

            return request;

        } catch (Exception e) {
            logger.error("构建玩家弃牌完成请求失败，手牌ID: {}", hand.getId(), e);
            // 返回一个基础的完成请求，确保手牌能够正常结束
            HandCompletionRequest fallbackRequest = new HandCompletionRequest();
            fallbackRequest.setCompletionType("player_fold");
            fallbackRequest.setFinalPot(hand.getCurrentPot() != null ? hand.getCurrentPot() : BigDecimal.ZERO);
            fallbackRequest.setRakeAmount(BigDecimal.ZERO);
            return fallbackRequest;
        }
    }

    /**
     * 计算Hero弃牌时的净损失 - 德州扑克筹码计算逻辑
     *
     * Hero弃牌的损失计算逻辑： - 基础损失 = Hero起始筹码 - Hero当前筹码 - 如果无法获取确切筹码信息，使用估算方法 - 损失为负数（表示亏损）
     */
    private BigDecimal calculateHeroLossFromFold(PokerHand hand, ActionRequest foldRequest) {
        try {
            // 方法一：使用Action请求中的stack_after信息
            if (foldRequest.getAfterStack() != null && hand.getHeroStackStart() != null) {
                // 净损失 = 起始筹码 - 剩余筹码（负数表示损失）
                BigDecimal netResult = foldRequest.getAfterStack().subtract(hand.getHeroStackStart());
                logger.debug("使用精确筹码计算Hero损失：起始 {} - 剩余 {} = {}", hand.getHeroStackStart(),
                    foldRequest.getAfterStack(), netResult);
                return netResult;
            }

            // 方法二：估算损失（基于盲注和前注）
            BigDecimal estimatedLoss = BigDecimal.ZERO;

            // 如果有小盲注，假设Hero至少损失小盲注
            if (hand.getSmallBlind() != null) {
                estimatedLoss = estimatedLoss.subtract(hand.getSmallBlind());
            }

            // 如果有前注，加上前注损失
            if (hand.getAnte() != null) {
                estimatedLoss = estimatedLoss.subtract(hand.getAnte());
            }

            // 如果完全没有信息，使用大盲注的一半作为最小损失估算
            if (estimatedLoss.equals(BigDecimal.ZERO) && hand.getBigBlind() != null) {
                estimatedLoss = hand.getBigBlind().divide(new BigDecimal("2"), 2, BigDecimal.ROUND_HALF_UP).negate();
            }

            logger.debug("使用估算方法计算Hero损失：{}", estimatedLoss);
            return estimatedLoss;

        } catch (Exception e) {
            logger.warn("计算Hero弃牌损失时发生异常，使用默认值，手牌ID: {}", hand.getId(), e);
            // 异常时返回一个小的负数作为默认损失
            return new BigDecimal("-10");
        }
    }

    /**
     * @deprecated V4.10 已废弃 - ActionResponse已简化，不再需要此方法
     */
    /*
    private ActionResponse buildHandCompletedActionResponse(PokerHandAction savedAction,
        HandCompletionResponse completionResponse) {
        try {
            // 1. 构建序列信息 - 标识这是最后一个行动
            ActionResponse.SequenceInfo sequence =
                new ActionResponse.SequenceInfo(savedAction.getActionSequence(), savedAction.getGlobalSequence(),
                    savedAction.getGlobalSequence() + "/FINAL" // 标识为最终行动
                );

            // 2. 构建游戏状态 - 表示手牌已完成
            ActionResponse.GameState gameState = new ActionResponse.GameState();
            // TODO: 添加手牌完成相关的状态标识（需要扩展GameState类）
            // gameState.setHandStatus("completed"); // 手牌状态：已完成
            // gameState.setCompletionType("hero_fold"); // 完成原因：Hero弃牌  
            // gameState.setHandCompleted(true); // 明确标识手牌已结束
            logger.info("构建Hero弃牌完成响应 - 使用FINAL序列标识");

            // 3. 构建验证结果 - 所有验证通过，但手牌已结束
            ActionResponse.ValidationResult validationResult =
                new ActionResponse.ValidationResult(true, true, false); // 最后一个参数表示不能继续行动

            // 4. 构建最终响应
            ActionResponse response = new ActionResponse(savedAction.getId(), sequence, gameState, validationResult);

            // 5. 附加完成信息（如果需要的话）
            // 可以在这里添加completion相关的额外信息

            logger.info("构建Hero弃牌响应成功 - 行动ID: {}, 手牌已自动完成", savedAction.getId());
            return response;

        } catch (Exception e) {
            logger.error("构建Hero弃牌响应失败，行动ID: {}", savedAction.getId(), e);
            // 返回一个基础的响应，确保客户端能收到正确的信息
            ActionResponse.SequenceInfo fallbackSequence =
                new ActionResponse.SequenceInfo(1, savedAction.getGlobalSequence(), "FINAL");
            ActionResponse.GameState fallbackGameState = new ActionResponse.GameState();
            // TODO: fallbackGameState.setHandCompleted(true); // 需要扩展GameState类
            ActionResponse.ValidationResult fallbackValidation = new ActionResponse.ValidationResult(true, true, false);
            return new ActionResponse(savedAction.getId(), fallbackSequence, fallbackGameState, fallbackValidation);
        }
    }
    */

    /**
     * V4.8: 已废弃 - 盲注不再由后端自动记录
     * 盲注由前端通过 RECORD_ACTION_REQ 主动发起
     * 
     * @deprecated V4.8: 盲注不再自动创建，此方法保留仅供参考
     */
    @Deprecated
    private void createBlindActions(PokerHand hand, CreateHandRequest request) {
        // V4.8: 此方法已废弃
        // 盲注由前端通过 RECORD_ACTION_REQ 主动发起
        // small_blind 和 big_blind 字段已从 CreateHandRequest 中移除
        logger.warn("createBlindActions() is deprecated in V4.8 - blinds should be posted via RECORD_ACTION_REQ");
    }

    /**
     * Create single blind action record
     */
    private PokerHandAction createBlindAction(PokerHand hand, CreateHandRequest.PlayerInfo player, String actionType,
        BigDecimal amount, int globalSequence, BigDecimal potAfter) {
        Long userId = findUserIdByPlayerId(player.getPlayerId());

        PokerHandAction action = new PokerHandAction(hand.getId(), player.getSeatNumber(), player.getPlayerId(), userId,
            PokerHandAction.STREET_PREFLOP, globalSequence,  // actionSequence = globalSequence for preflop blinds
            globalSequence,  // globalSequence
            actionType);

        action.setAmount(amount);
        action.setStackAfter(player.getStack().subtract(amount));  // Player stack decreases
        action.setPotAfter(potAfter);  // Use correct pot amount passed in
        action.setThinkTimeSeconds(0);  // Blinds have no think time

        return action;
    }

    /**
     * 计算小盲位置座位号
     */
    private int calculateSmallBlindSeat(int buttonSeat, int playerCount) {
        if (playerCount == 2) {
            return buttonSeat;  // 两人桌：按钮位即小盲位
        }
        return (buttonSeat % playerCount) + 1;  // 按钮位顺时针下一位
    }

    /**
     * 计算大盲位置座位号
     */
    private int calculateBigBlindSeat(int buttonSeat, int playerCount) {
        if (playerCount == 2) {
            return (buttonSeat % playerCount) + 1;  // 两人桌：按钮位顺时针下一位
        }
        return ((buttonSeat + 1) % playerCount) + 1;  // 小盲位顺时针下一位
    }

    /**
     * 根据座位号找到对应玩家
     */
    private CreateHandRequest.PlayerInfo findPlayerBySeat(List<CreateHandRequest.PlayerInfo> players, int seatNumber) {
        return players.stream().filter(p -> p.getSeatNumber() == seatNumber).findFirst().orElse(null);
    }

    /**
     * 创建行动记录 - 使用双字段设计简化逻辑
     *
     * 架构优势： 1. 直接使用前端提供的playerId，无需复杂JSON解析 2. 通过高效的用户查询获取userId，支持快速统计分析 3. 双字段设计兼顾业务清晰性和查询性能 4. 减少代码复杂度，提高维护性
     */
    private PokerHandAction createActionRecord(PokerHand hand, ActionRequest request) {
        String frontendPlayerId = request.getPlayerId();
        Long databaseUserId = findUserIdByPlayerId(frontendPlayerId);
        // 创建行动记录：使用双字段构造函数，考虑已存在的盲注记录
        int currentStreetSequence = calculateStreetSequence(hand.getId(), hand.getCurrentStreet()) + 1;
        // V4.9: 使用线程安全的方法获取 global_sequence
        int currentGlobalSequence = getNextGlobalSequence(hand.getId());

        // 通过playerId推导座位号（座位号字段已从ActionRequest移除）
        Integer seatNumber = findSeatNumberByPlayerId(hand.getId(), frontendPlayerId);
        
        PokerHandAction action =
            new PokerHandAction(hand.getId(), seatNumber, frontendPlayerId, databaseUserId,
                hand.getCurrentStreet(), currentStreetSequence, currentGlobalSequence, request.getActionType());
        // 设置行动详细信息
        action.setAmount(request.getAmount());
        
        // V4.1: 容错处理 - 如果客户端没有提供after_stack/after_pot，使用默认值
        BigDecimal afterStack = request.getAfterStack();
        if (afterStack == null && request.getBeforeStack() != null) {
            // 对于fold/check：after_stack = before_stack
            // 对于其他行动：after_stack = before_stack - amount
            if ("fold".equalsIgnoreCase(request.getActionType()) || "check".equalsIgnoreCase(request.getActionType())) {
                afterStack = request.getBeforeStack();
            } else {
                BigDecimal amount = request.getAmount() != null ? request.getAmount() : BigDecimal.ZERO;
                afterStack = request.getBeforeStack().subtract(amount);
            }
        }
        
        // V4.29: 防止筹码为负数（全下保护）
        if (afterStack != null && afterStack.compareTo(BigDecimal.ZERO) < 0) {
            logger.warn("检测到筹码为负数，修正为0（全下）: playerId={}, beforeStack={}, amount={}, calculatedStack={}", 
                    request.getPlayerId(), request.getBeforeStack(), request.getAmount(), afterStack);
            afterStack = BigDecimal.ZERO;
        }
        
        action.setStackAfter(afterStack);
        
        BigDecimal afterPot = request.getAfterPot();
        if (afterPot == null && request.getBeforePot() != null) {
            // 对于fold/check：after_pot = before_pot
            // 对于其他行动：after_pot = before_pot + amount
            if ("fold".equalsIgnoreCase(request.getActionType()) || "check".equalsIgnoreCase(request.getActionType())) {
                afterPot = request.getBeforePot();
            } else {
                BigDecimal amount = request.getAmount() != null ? request.getAmount() : BigDecimal.ZERO;
                afterPot = request.getBeforePot().add(amount);
            }
        }
        action.setPotAfter(afterPot);
        
        action.setThinkTimeSeconds(request.getThinkTimeSeconds());
        return action;
    }

    /**
     * 从数据库实时计算总行动数（替代被移除的字段）
     * @deprecated 不推荐直接使用，应使用 getNextGlobalSequence 来确保原子性
     */
    @Deprecated
    private int calculateTotalActionsFromDatabase(Long handId) {
        try {
            // 从 poker_hand_actions 表统计该手牌的总行动数
            return (int)pokerHandActionDao.countByHandId(handId);
        } catch (Exception e) {
            logger.warn("Failed to calculate total actions for hand {}, using default 0", handId, e);
            return 0;
        }
    }
    
    /**
     * V4.9: 获取下一个 global_sequence（线程安全版本）
     * 
     * 原理：通过手牌级别的锁（handLocks）保证同一手牌的行动串行执行，
     * 从而避免了并发情况下多个线程获取到相同 global_sequence 的问题。
     * 
     * @param handId 手牌ID
     * @return 下一个 global_sequence
     */
    private int getNextGlobalSequence(Long handId) {
        // 注意：此方法必须在 recordAction 的锁保护下调用
        // handLocks 已经保证了同一手牌的串行执行
        try {
            // 查询当前最大的 global_sequence
            return (int)pokerHandActionDao.countByHandId(handId) + 1;
        } catch (Exception e) {
            logger.warn("Failed to get next global sequence for hand {}, using default 1", handId, e);
            return 1;
        }
    }

    /**
     * 实时计算手牌持续时间（替代被移除的字段）
     */
    private int calculateHandDuration(PokerHand hand) {
        if (hand.getCreateTime() != null && hand.getCompletedTime() != null) {
            return (int)((hand.getCompletedTime() - hand.getCreateTime()) / 1000);
        }
        return 0;
    }

    /**
     * 通过player_id获取座位号 - 从players_info JSON中查找
     * 
     * @param handId 手牌ID
     * @param playerId 玩家ID
     * @return 座位号，如果不存在返回null
     */
    private Integer findSeatNumberByPlayerId(Long handId, String playerId) {
        if (playerId == null) {
            return null;
        }
        try {
            // 从数据库读取手牌信息
            PokerHand hand = pokerHandDao.findById(handId).orElse(null);
            if (hand == null || hand.getPlayersInfo() == null) {
                logger.warn("Hand {} not found or players_info is null, using fallback for playerId: {}", handId, playerId);
                return getDefaultSeatNumber(playerId);
            }
            
            // 解析players_info JSON，查找对应玩家的座位号
            try {
                List<Map<String, Object>> players = objectMapper.readValue(
                    hand.getPlayersInfo(), 
                    objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
                );
                
                for (Map<String, Object> player : players) {
                    String pid = (String) player.get("player_id");
                    if (playerId.equals(pid)) {
                        Object seatObj = player.get("seat_number");
                        if (seatObj instanceof Integer) {
                            return (Integer) seatObj;
                        } else if (seatObj != null) {
                            return Integer.valueOf(seatObj.toString());
                        }
                    }
                }
                
                logger.warn("PlayerId {} not found in players_info for hand {}, using fallback", playerId, handId);
                return getDefaultSeatNumber(playerId);
                
            } catch (Exception e) {
                logger.error("Failed to parse players_info JSON for hand {}, using fallback", handId, e);
                return getDefaultSeatNumber(playerId);
            }
        } catch (Exception e) {
            logger.warn("Failed to find seat number for playerId: {} in hand: {}", playerId, handId, e);
            return getDefaultSeatNumber(playerId);
        }
    }

    /**
     * 获取默认座位号 - 当无法从数据库查询时的fallback
     * 注意：这是一个fallback方法，实际座位号应从players_info读取
     */
    private Integer getDefaultSeatNumber(String playerId) {
        if (playerId == null) return 1;
        // 简单的Hash算法生成座位号（1-10）
        // 注意：这只是fallback，不应该在正常流程中使用
        return Math.abs(playerId.hashCode() % 10) + 1;
    }

    /**
     * 高效的用户ID查找 - 使用缓存优化，带重试机制
     *
     * @param playerId 前端提供的玩家ID
     * @return 数据库用户ID，如果不存在返回null
     */
    private Long findUserIdByPlayerId(String playerId) {
        if (playerId == null || playerId.trim().isEmpty()) {
            return null;
        }

        try {
            // 第一次尝试：从缓存获取
            Long userId = pokerUserOperator.getPlayerIdToUserIdMapping(playerId);
            if (userId != null) {
                return userId;
            }

            // 第二次尝试：清除缓存后重新查询
            pokerUserOperator.clearPlayerIdCache(playerId);
            userId = pokerUserOperator.getPlayerIdToUserIdMapping(playerId);

            if (userId == null) {
                // V4.7: 如果玩家不存在，自动创建一个临时玩家记录
                logger.info("玩家不存在，自动创建: playerId={}", playerId);
                userId = pokerUserOperator.getOrCreatePlayerByPlayerId(playerId);
            }
            return userId;

        } catch (Exception e) {
            logger.error("查询或创建用户ID失败: playerId={}", playerId, e);
            return null;
        }
    }

    /**
     * @deprecated 已被findUserIdByPlayerId替代，保留用于兼容性 从手牌信息中根据座位号和玩家名找到对应的数据库用户ID 通过前端的 player_id 关联到数据库中的用户记录
     */
    @Deprecated
    private Long findPlayerIdFromHandInfo(PokerHand hand, ActionRequest request) {
        try {
            // 注意：playersInfo字段已移除，此方法已废弃
            logger.warn("playersInfo field has been removed, this method is deprecated");
            String playersInfoJson = null; // 字段已移除
            if (playersInfoJson == null || playersInfoJson.trim().isEmpty()) {
                logger.warn("手牌{}的玩家信息为空，无法找到playerId", hand.getId());
                return null;
            }
            // 解析玩家信息JSON
            CreateHandRequest.PlayerInfo[] players =
                objectMapper.readValue(playersInfoJson, CreateHandRequest.PlayerInfo[].class);
            // 注意：seat_number已被移除，此处匹配逻辑已废弃
            CreateHandRequest.PlayerInfo matchedPlayer = null;
            // 如果座位号没匹配上，尝试按玩家ID匹配
            if (matchedPlayer == null && request.getPlayerId() != null) {
                for (CreateHandRequest.PlayerInfo player : players) {
                    if (request.getPlayerId().equals(player.getPlayerId())) {
                        matchedPlayer = player;
                        break;
                    }
                }
            }
            if (matchedPlayer != null && matchedPlayer.getPlayerId() != null) {
                // 使用前端的 player_id 查找用户
                String playerId = matchedPlayer.getPlayerId().trim();

                Optional<PokerUser> user = pokerUserOperator.findUserByPlayerId(playerId);
                if (user.isPresent()) {
                    return user.get().getId();
                } else {
                    logger.warn("未找到前端playerId={}对应的数据库用户记录", playerId);
                    return null;
                }
            }
            logger.warn("无法在手牌{}中找到玩家{}的匹配信息", hand.getId(), request.getPlayerId());
            return null;

        } catch (Exception e) {
            logger.error("解析手牌{}的玩家信息时发生错误: {}", hand.getId(), e.getMessage(), e);
            return null;
        }
    }

    /**
     * 统一更新手牌状态并持久化 - 纯粹的状态保存处理
     *
     * 完整的状态更新流程： 1. 基础统计更新（每个行动都必须的） 2. 根据行动类型进行特定的状态更新 3. Hero相关的特殊处理 4. 持久化更新后的状态到数据库 5. 记录状态更新结果到日志
     *
     * @param hand    手牌对象（内存中）
     * @param request 行动请求（包含前端计算的状态）
     * @param log     日志对象（用于记录状态变化）
     */
    /**
     * 统一更新手牌状态并持久化 - 前端主导，后端记录模式
     *
     * 设计原则： - 前端负责所有游戏逻辑计算和状态维护 - 后端只负责接收、验证和保存前端计算的状态 - 不进行任何复杂的德扑逻辑计算，避免前后端逻辑不一致 - 专注于数据完整性和为AI分析提供丰富的上下文信息
     *
     * 简化的状态保存流程： 1. 接收前端计算的完整桌面状态 2. 基础数据验证（保证数据合理性） 3. 直接保存到手牌对象 4. 持久化到数据库 5. 记录完整的状态变化日志
     *
     * @param hand    手牌对象（内存中）
     * @param request 包含前端计算的完整桌面状态的行动请求
     * @param log     日志对象（用于记录状态变化）
     */
    private void updateHandStateAndPersist(PokerHand hand, ActionRequest request, PlainLog log) {
        // Step 1: 接收并保存前端计算的完整桌面状态
        saveCompleteGameStateFromFrontend(hand, request);

        // Step 2: 基础统计更新（行动计数等后端维护的统计）
        updateBackendMaintainedStatistics(hand, request);

        // Step 3: 持久化更新后的状态到数据库  
        updatePokerHand(hand);

        // Step 4: 记录完整的状态保存结果到日志（为AI分析提供丰富信息）
        logCompleteGameState(hand, request, log);

        // 注意：活跃玩家数和总行动数字段已移除
    }

    /**
     * 简化版的手牌状态更新 - 只进行必要的状态保存，不记录详细日志
     */
    private void updateHandStateAndPersistSimplified(PokerHand hand, ActionRequest request) {
        // Step 1: 接收并保存前端计算的完整桌面状态
        saveCompleteGameStateFromFrontend(hand, request);

        // Step 2: 基础统计更新（行动计数等后端维护的统计）
        updateBackendMaintainedStatistics(hand, request);

        // Step 3: 持久化更新后的状态到数据库  
        updatePokerHand(hand);

        // 注意：不记录详细的游戏状态日志，保持日志简洁
    }

    /**
     * 保存前端计算的完整游戏状态 - 核心状态接收方法
     *
     * 职责： - 接收前端传递的所有游戏状态信息 - 进行基础的数据合理性验证 - 直接保存到PokerHand对象中 - 不进行任何游戏逻辑计算
     *
     * 保存的状态包括： - 底池信息（potAfter, potBefore） - 投注信息（toCall, minRaise） - 玩家信息（activePlayers, nextActionSeat） -
     * 游戏进度（currentStreet, bettingRoundComplete） - 牌面信息（boardCards） - AI分析所需的上下文（position, effectiveStack, potOdds）
     *
     * @param hand    手牌对象
     * @param request 包含完整桌面状态的行动请求
     */
    private void saveCompleteGameStateFromFrontend(PokerHand hand, ActionRequest request) {
        // === 1. 底池状态保存 ===
        if (request.getAfterPot() != null) {
            validateAmount("底池金额", request.getAfterPot());
            hand.setCurrentPot(request.getAfterPot());
        }

        // === 2. 投注状态保存 ===
        // 注意：以下字段已从 poker_hands 表中移除，不再存储：
        // - toCall: 需跟注金额（应用层实时计算）
        // - minRaise: 最小加注金额（应用层实时计算）
        // - activePlayers: 活跃玩家数（应用层实时计算）
        // 这些信息将在游戏进行过程中实时计算，提高数据一致性

        // 注意：setNextActionSeat字段已移除，下一个行动座位将在应用层实时计算
        // if (request.getNextActionSeat() != null) {
        //     hand.setNextActionSeat(request.getNextActionSeat());
        // }

        // === 4. 游戏进度状态保存 ===
        // 注意：街道和公共牌信息现在通过街道转换接口单独管理
        // 行动记录不再更新这些游戏状态信息

        // 注意：toCall、activePlayers字段已移除
    }

    /**
     * 更新后端维护的统计信息 - 仅限于后端需要维护的基础统计
     *
     * 后端维护的统计（不依赖前端）： - 总行动数递增（用于行动序列号生成） - 行动时间戳记录 - Hero相关的特殊标记
     *
     * 不再维护的复杂状态（由前端负责）： - 底池计算、toCall计算、活跃玩家数等游戏逻辑状态 - 街道转换、投注轮完成等复杂判断
     *
     * @param hand    手牌对象
     * @param request 行动请求
     */
    private void updateBackendMaintainedStatistics(PokerHand hand, ActionRequest request) {
        // 1. 总行动数递增（用于生成行动序列号）
        // 注意：incrementActionCount方法已废弃，行动数将从 poker_hand_actions 表实时计算
        // hand.incrementActionCount(); // 已废弃

        // 2. Hero相关标记（用于数据分析）
        if (isHeroAction(hand, request)) {
        }

        // 注意：总行动数已移除，可从 poker_hand_actions 表实时查询
    }

    /**
     * 记录完整的游戏状态到日志 - 为AI分析提供丰富的上下文信息
     *
     * 记录内容： - 基础行动信息（座位、类型、金额） - 完整桌面状态（底池、投注、玩家） - 牌面和位置信息 - AI分析所需的计算指标（赔率、筹码深度等）
     *
     * 这些日志是AI决策模型的重要输入数据
     *
     * @param hand    手牌对象
     * @param request 行动请求
     * @param log     日志对象
     */
    private void logCompleteGameState(PokerHand hand, ActionRequest request, PlainLog log) {
        // === 基础行动信息 ===
        log.put("actionType", request.getActionType())
            .put("playerId", request.getPlayerId()).put("amount", request.getAmount())
            .put("thinkTime", request.getThinkTimeSeconds());

        // === 完整桌面状态 ===
        log.put("currentPot", hand.getCurrentPot()).put("currentStreet", hand.getCurrentStreet());
        // 注意：以下字段已移除，需要从应用层计算：toCall, minRaise, activePlayers, totalActions

        // === AI分析相关信息 ===
        // 注意：AI分析相关信息现在从手牌状态和应用层获取，不再依赖ActionRequest
        log.put("boardCards", hand.getBoardCards());

        // === Hero标记（用于AI训练数据标注） ===
        log.put("isHeroAction", isHeroAction(hand, request));
    }

    /**
     * 验证金额的合理性 - 基础数据验证
     *
     * @param fieldName 字段名称（用于错误信息）
     * @param amount    金额
     */
    private void validateAmount(String fieldName, BigDecimal amount) {
        if (amount != null && amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException(fieldName + "不能为负数: " + amount);
        }
    }

    /**
     * 验证玩家数量的合理性 - 基础数据验证
     *
     * @param fieldName   字段名称（用于错误信息）
     * @param playerCount 玩家数量
     */
    private void validatePlayerCount(String fieldName, Integer playerCount) {
        if (playerCount != null && (playerCount < 0 || playerCount > 10)) {
            throw new IllegalArgumentException(fieldName + "超出合理范围(0-10): " + playerCount);
        }
    }

    // === 删除的复杂方法说明 ===
    // 以下方法在"前端主导，后端记录"模式下不再需要：
    // - handleCheckAction, handleCallAction, handleBetAction, handleRaiseAction, handleAllinAction
    // - updateStreetStatisticsForFold, checkIfBettingRoundComplete
    // 
    // 原因：这些复杂的德扑游戏逻辑现在完全由前端负责
    // 后端只需要保存前端计算的结果，不再进行任何游戏规则计算

    /**
     * 获取已完成的街道数量 用于估算街道内行动序号
     */
    private int getCompletedStreetCount(String currentStreet) {
        switch (currentStreet) {
            case PokerHand.STREET_PREFLOP:
                return 0; // preflop 是第一个街道
            case PokerHand.STREET_FLOP:
                return 1; // preflop 已完成
            case PokerHand.STREET_TURN:
                return 2; // preflop, flop 已完成
            case PokerHand.STREET_RIVER:
                return 3; // preflop, flop, turn 已完成
            default:
                return 0; // 未知街道，假设是第一个
        }
    }

    /**
     * 计算预期总行动数 - 德州扑克行动估算逻辑
     *
     * 用途： - 为前端提供进度指示 - 用于序列信息的完整性展示（如 "5/12" 表示当前第5个行动，共预期12个） - 分析和统计使用
     *
     * 估算原则： - 基于当前活跃玩家数和街道情况 - 考虑德扑的基本行动模式 - 提供合理的上界估算，避免过度精确的计算
     */
    private Integer calculateTotalExpectedActions(PokerHand hand) {
        try {
            if (hand == null) {
                return 10; // 默认估算值
            }

            // 获取基础信息
            String currentStreet = hand.getCurrentStreet();
            // 注意：活跃玩家数和总行动数已移除，使用默认值
            Integer activePlayers = 6; // 默认6人桌
            Integer totalActions = calculateTotalActionsFromDatabase(hand.getId());

            // 方法一：基于当前行动数的动态估算
            if (totalActions != null && totalActions > 0) {
                // 如果已有较多行动，基于当前进度估算
                if (currentStreet != null) {
                    int streetProgress = getStreetProgress(currentStreet);
                    if (streetProgress > 0) {
                        // 估算总数 = 当前行动数 / 当前进度比例
                        double progressRatio = streetProgress / 4.0; // 4个街道
                        int estimatedTotal = (int)(totalActions / progressRatio * 1.2); // 1.2 作为缓冲
                        return Math.max(estimatedTotal, totalActions + activePlayers); // 至少比当前多几个行动
                    }
                }

                // 简单估算：当前行动数 + 剩余可能的行动数
                return totalActions + (activePlayers * 2);
            }

            // 方法二：基于德扑规则的理论估算
            // 每个街道平均行动数：preflop(多), flop(中), turn(少), river(少)
            int estimatedPreflopActions = Math.max(activePlayers, activePlayers + (activePlayers / 2)); // 翻前通常较多
            int estimatedPostflopActions = activePlayers * 2; // 翻后每街道平均

            int totalEstimate = estimatedPreflopActions + (estimatedPostflopActions * 3); // 3个翻后街道

            // 应用修正系数
            if (activePlayers <= 2) {
                totalEstimate = totalEstimate / 2; // 短桌行动较少
            } else if (activePlayers >= 8) {
                totalEstimate = (int)(totalEstimate * 1.3); // 满桌行动较多
            }

            logger.debug("估算总行动数 - 手牌ID: {}, 活跃玩家: {}, 当前街道: {}, 已有行动: {}, 估算总数: {}",
                hand.getId(), activePlayers, currentStreet, totalActions, totalEstimate);

            return Math.max(totalEstimate, 8); // 最少8个行动

        } catch (Exception e) {
            logger.debug("计算预期总行动数时发生异常，使用默认值 - 手牌ID: {}", hand != null ? hand.getId() : "null", e);
            return 12; // 默认12个行动
        }
    }

    /**
     * 获取街道进度（1-4） 用于估算总行动数
     */
    private int getStreetProgress(String street) {
        switch (street) {
            case PokerHand.STREET_PREFLOP:
                return 1;
            case PokerHand.STREET_FLOP:
                return 2;
            case PokerHand.STREET_TURN:
                return 3;
            case PokerHand.STREET_RIVER:
                return 4;
            default:
                return 1;
        }
    }

    /**
     * @deprecated V4.10 已废弃 - ActionResponse已简化，不再需要GameState
     */
    /*
    private ActionResponse.GameState buildGameState(PokerHand hand) {
        ActionResponse.GameState gameState = new ActionResponse.GameState();
        gameState.setCurrentStreet(hand.getCurrentStreet());
        // 注意：以下字段已从 poker_hands 表中移除，需要在应用层实时计算
        // gameState.setActivePlayers(hand.getActivePlayers());
        // gameState.setNextActionSeat(hand.getNextActionSeat());
        // gameState.setToCall(hand.getToCall());
        // gameState.setMinRaise(hand.getMinRaise());

        // 注意：toCall字段已移除，底池赔率计算需要从应用层提供
        // 如果需要计算底池赔率，可从 poker_hand_actions 表查询最新的toCall值
        gameState.setPotOdds(BigDecimal.ZERO); // 使用默认值

        return gameState;
    }
    */

    private void validateHandCompletion(PokerHand hand, HandCompletionRequest request) {
        if (!hand.isActive()) {
            throw new IllegalArgumentException("Hand is not in active state");
        }

        // Hero net result字段已移除，改用final_results数组
        // if (request.getFinalResults() == null || request.getFinalResults().isEmpty()) {
        //     throw new IllegalArgumentException("Final results are required");
        // }

        if (request.getFinalPot() == null || request.getFinalPot().compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Final pot must be non-negative");
        }
    }

    private Integer calculateStreetsReached(PokerHand hand) {
        String street = hand.getCurrentStreet();
        return switch (street) {
            case "preflop" -> 1;
            case "flop" -> 2;
            case "turn" -> 3;
            case "river" -> 4;
            default -> 1;
        };
    }

    /**
     * ✅ V4.8新增：将卡牌列表转换为逗号分隔的字符串
     * 
     * @param cardsList 卡牌列表（如["49", "6"]）
     * @return 逗号分隔的字符串（如"49,6"）或空字符串
     */
    private String convertCardsListToString(List<String> cardsList) {
        if (cardsList == null || cardsList.isEmpty()) {
            return "";
        }
        return String.join(",", cardsList);
    }

    // 内部辅助类
    // ActionValidationResult 类已删除 - 验证方法现在直接抛出异常

    /**
     * 确保所有参与玩家的用户记录存在，如果不存在则自动创建
     */
    private void ensureAllPlayersExist(List<CreateHandRequest.PlayerInfo> players) {
        for (CreateHandRequest.PlayerInfo player : players) {
            String playerId = player.getPlayerId();

            if (playerId == null || playerId.trim().isEmpty()) {
                continue; // 跳过无效的playerId
            }

            try {
                // 查找用户是否存在
                Optional<PokerUser> existingUser = pokerUserOperator.findUserByPlayerId(playerId);

                if (existingUser.isEmpty()) {
                    // 用户不存在，创建新用户（V4.1: 使用playerId作为默认名称）
                    try {
                        pokerUserOperator.createUser(playerId, playerId);  // V4.1: player_name字段已移除
                    } catch (IllegalArgumentException e) {
                        if (e.getMessage().contains("already exists")) {
                            // 并发创建冲突，清除缓存重试
                            pokerUserOperator.clearPlayerIdCache(playerId);
                        } else {
                            throw e;
                        }
                    }
                }

            } catch (Exception e) {
                // Hero概念已移除，所有玩家创建失败都记录警告但不中止
                // 这允许游戏继续进行，即使某些玩家的用户记录创建失败
                logger.warn("玩家用户创建失败: playerId={}, error={}", playerId, e.getMessage());
            }
        }
    }

    /**
     * 计算街道序列号 - 查询数据库中当前街道的实际行动数量
     *
     * @param handId 手牌ID
     * @param street 街道
     * @return 街道内的序列号
     */
    private int calculateStreetSequence(Long handId, String street) {
        try {
            // 查询当前街道已有的行动数量
            long currentStreetActions = pokerHandActionDao.countByHandIdAndStreet(handId, street);
            // 返回下一个序列号
            return (int)currentStreetActions;
        } catch (Exception e) {
            logger.debug("计算街道序列号时发生异常 - 手牌ID: {}, 街道: {}", handId, street, e);
            return 1; // 异常时返回默认值
        }
    }

    /**
     * 检测是否为Hero的行动 - 简化版本
     *
     * @param hand    手牌对象
     * @param request 行动请求
     * @return 是否为Hero行动
     */
    private boolean isHeroAction(PokerHand hand, ActionRequest request) {
        try {
            // 主要方法：玩家ID比对（推荐方法）
            if (request.getPlayerId() != null) {
                // 简化判断：如果playerId包含"hero"关键字
                return request.getPlayerId().toLowerCase().contains("hero");
            }

            return false;
        } catch (Exception e) {
            logger.debug("判断Hero行动时发生异常", e);
            return false;
        }
    }

}